[iOS 8] UIPresentationController でカスタムのモーダル表示を実装する
UIPresentationController とは
UIPresentationController は iOS 8 から追加された、View Controller の上のレイヤーにモーダルのような形で画面を表示する機能を提供する View Controller です。
iPad ではよく目にする機会が多いですが、下図のように View Controller の上に重なる感じで表示される画面のことです。
このような機能は、これまでは UIPopoverController のように、カスタマイズ不可能な形で提供されていました。iOS 8 では UIPresentationController が追加され、自由な表示・アニメーションのモーダルが表示できるようになりました。
なお、UIPresentationController は抽象クラスで、標準では上記 UIPopoverController の代替として UIPopoverPresentationController が実装クラスとして提供されています。この抽象クラスを実装することで自由な表示・アニメーションのモーダルが実現できます。
実装ソースは気まぐれで GitHub に公開したので、お急ぎのかたはこちらを Clone していただければと思います。
2015/11/21 Xcode 7.1.1 で動作するように修正しました。
実装する
早速実装します。まずは View Controller から実装です。
class ViewController: UIViewController, UIViewControllerTransitioningDelegate { @IBAction func buttonDidTouch(sender: AnyObject) { // 新しい View Controller をモーダル表示する let controller: UINavigationController! = self.storyboard?.instantiateViewControllerWithIdentifier("NavigationController") as? UINavigationController controller.modalPresentationStyle = .Custom controller.transitioningDelegate = self self.presentViewController(controller, animated: true, completion: { }) } // MARK: UIViewControllerTransitioningDelegate func presentationControllerForPresentedViewController(presented: UIViewController, presentingViewController presenting: UIViewController, sourceViewController source: UIViewController) -> UIPresentationController? { return CustomPresentationController(presentedViewController: presented, presentingViewController: presenting) } }
ポイントは2点です。まず UIViewControllerTransitioningDelegate というプロトコルを追加します。このプロトコルの presentationControllerForPresentedViewController:presentingViewController:sourceViewController: メソッドを実装し、UIPresentationController の実装クラスのインスタンスを返します。こうすることで presentViewController:animated:completion: を実行するときの振る舞いに UIPresentationController の実装クラスが適用されます。
上記メソッドで presented やら presenting やら出てきていますが、presented が呼び出し先の View Controller、presenting が呼び出し元の View Controller です。よく出てくるので記憶しておいてください。
次に UIPresentationController を実装します。ちょっと長いですが、コメントを随所に書いているので読んでいくと理解できると思います。
class CustomPresentationController: UIPresentationController { // 呼び出し元の View Controller の上に重ねるオーバーレイ View var overlay: UIView! // 表示トランジション開始前に呼ばれる override func presentationTransitionWillBegin() { let containerView = self.containerView! self.overlay = UIView(frame: containerView.bounds) self.overlay.gestureRecognizers = [UITapGestureRecognizer(target: self, action: "overlayDidTouch:")] self.overlay.backgroundColor = UIColor.blackColor() self.overlay.alpha = 0.0 containerView.insertSubview(self.overlay, atIndex: 0) // トランジションを実行 presentedViewController.transitionCoordinator()?.animateAlongsideTransition({ [unowned self] context in self.overlay.alpha = 0.5 }, completion: nil) } // 非表示トランジション開始前に呼ばれる override func dismissalTransitionWillBegin() { self.presentedViewController.transitionCoordinator()?.animateAlongsideTransition({ [unowned self] context in self.overlay.alpha = 0.0 }, completion: nil) } // 非表示トランジション開始後に呼ばれる override func dismissalTransitionDidEnd(completed: Bool) { if completed { self.overlay.removeFromSuperview() } } // 子のコンテナのサイズを返す override func sizeForChildContentContainer(container: UIContentContainer, withParentContainerSize parentSize: CGSize) -> CGSize { return CGSize(width: parentSize.width / 2, height: parentSize.height) } // 呼び出し先の View Controller の Frame を返す override func frameOfPresentedViewInContainerView() -> CGRect { var presentedViewFrame = CGRectZero let containerBounds = containerView!.bounds presentedViewFrame.size = self.sizeForChildContentContainer(self.presentedViewController, withParentContainerSize: containerBounds.size) presentedViewFrame.origin.x = containerBounds.size.width - presentedViewFrame.size.width presentedViewFrame.origin.y = containerBounds.size.height - presentedViewFrame.size.height return presentedViewFrame } // レイアウト開始前に呼ばれる override func containerViewWillLayoutSubviews() { overlay.frame = containerView!.bounds self.presentedView()!.frame = self.frameOfPresentedViewInContainerView() } // レイアウト開始後に呼ばれる override func containerViewDidLayoutSubviews() { } // オーバーレイの View をタッチしたときに呼ばれる func overlayDidTouch(sender: AnyObject) { self.presentedViewController.dismissViewControllerAnimated(true, completion: nil) } }
解説します。上記で実装しているメソッドは、overlayDidTouch: 以外はオーバーライドしたメソッドになります。
presentationTransitionWillBegin: と dismissalTransitionWillBegin: は呼び出し先の View Controller (presentedViewController) を表示/非表示するトランジション開始前に呼ばれます。ここではオーバーレイの View (黒い半透明の View) をフェードイン/フェードアウトするアニメーションを実装しています。dismissalTransitionDidEnd: では、非表示にするトランジションの終了後にオーバーレイで重ねている View を削除しています。なお、 overlayDidTouch: でオーバーレイをタッチすると非表示にできるようにしています。
frameOfPresentedViewInContainerView: では、呼び出し先の View Controller (presentedViewController) の表示位置を決めています。sizeForChildContentContainer:withParentContainerSize: で決めたサイズを利用し、左側にちょっと出るように設定しています。
ここまで実装すると次のように動作します。
まとめ
表示位置もアニメーションも自由自在に決めれるので、UI や UX に幅が広がりました。デザイナーも覚えておいたほうが良いと思います。
おまけ
アニメーションは UIViewControllerAnimatedTransitioning の実装クラスを使うと書き換えることができます。
class ViewController: UIViewController, UIViewControllerTransitioningDelegate { // MARK: UIViewControllerTransitioningDelegate func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? { return CustomAnimatedTransitioning(isPresent: true) } func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { return CustomAnimatedTransitioning(isPresent: false) } }
class CustomAnimatedTransitioning: NSObject, UIViewControllerAnimatedTransitioning { let isPresent: Bool init(isPresent: Bool) { self.isPresent = isPresent } func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval { return 0.3 } func animateTransition(transitionContext: UIViewControllerContextTransitioning) { if isPresent { animatePresentTransition(transitionContext) } else { animateDissmissalTransition(transitionContext) } } func animatePresentTransition(transitionContext: UIViewControllerContextTransitioning) { let presentingController: UIViewController! = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) let presentedController: UIViewController! = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) let containerView: UIView! = transitionContext.containerView() containerView.insertSubview(presentedController.view, belowSubview: presentingController.view) //適当にアニメーション UIView.animateWithDuration(self.transitionDuration(transitionContext), animations: { presentedController.view.frame.origin.x -= containerView.bounds.size.width }, completion: { finished in transitionContext.completeTransition(true) }) } func animateDissmissalTransition(transitionContext: UIViewControllerContextTransitioning) { let presentedController: UIViewController! = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) let containerView: UIView! = transitionContext.containerView() //適当にアニメーション UIView.animateWithDuration(self.transitionDuration(transitionContext), animations: { presentedController.view.frame.origin.x = containerView.bounds.size.width }, completion: { finished in transitionContext.completeTransition(true) }) } }
こんな感じです。左側からニュッと出てくるようにしてみました。
参考
- UIPresentationController Class Reference | iOS Developer Library
- UIViewControllerTransitioningDelegate Protocol Reference | iOS Developer Library
- UIViewControllerAnimatedTransitioning Protocol Reference | iOS Developer Library
- iOS8 presentation controllers | Dative Studios
- iOS8 Day-by-Day :: Day 24 :: Presentation Controllers | shinobi controls
- [iOS]画面遷移のアニメーションをカスタムする | Seesaa京都アプリエンジニアブログ